feat(share): workspace-scoped collaborative sessions with consensus voting#612
feat(share): workspace-scoped collaborative sessions with consensus voting#612
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #612 +/- ##
==========================================
- Coverage 80.82% 79.50% -1.33%
==========================================
Files 79 82 +3
Lines 26424 27808 +1384
==========================================
+ Hits 21358 22109 +751
- Misses 5066 5699 +633 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
Adds workspace-scoped sharing that upgrades Claudette’s legacy 1:1 remote-control pairing into multi-participant collaborative sessions, including room-based event fanout, turn locking, participant roster, and optional unanimous plan-approval voting.
Changes:
- Introduces a shared
Room/RoomRegistrycore (Rust) and server-side collab RPCs (join_session,leave_session,vote_plan_approval) to support multi-user sessions with broadcasted events. - Adds UI state + components for collaboration (participants roster, consensus vote tracking, workspace status icon updates, share-management modal, collaboration settings).
- Extends chat message schema to persist author identity (
author_participant_id,author_display_name) and wires author stamping/broadcasting across host + server bridges.
Reviewed changes
Copilot reviewed 56 out of 56 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| src/ui/src/utils/reconstructTurns.test.ts | Updates test fixtures to include new chat message author fields. |
| src/ui/src/utils/extractLatestCallUsage.test.ts | Updates test fixtures for author fields. |
| src/ui/src/utils/compactionSentinel.test.ts | Updates test fixtures for author fields. |
| src/ui/src/utils/checkpointUtils.test.ts | Updates test fixtures for author fields. |
| src/ui/src/utils/chatTurnFooter.test.ts | Updates test fixtures for author fields. |
| src/ui/src/types/remote.ts | Extends remote connection types to include participant_id. |
| src/ui/src/types/chat.ts | Extends ChatMessage with persisted author fields. |
| src/ui/src/stores/useAppStore.ts | Registers new collaboration slice in Zustand store. |
| src/ui/src/stores/useAppStore.test.ts | Updates store tests for author fields; includes collab-related test updates. |
| src/ui/src/stores/slices/collabSlice.ts | Adds collaboration slice: participants roster, turn holder, consensus voting state. |
| src/ui/src/services/tauri.ts | Adds share-management APIs (startShare, stopShare, listShares, moderation RPCs). |
| src/ui/src/hooks/useAgentStream.ts | Adds listeners for collab events and chat-message broadcasts; updates sentinel messages with author fields. |
| src/ui/src/components/sidebar/WorkspaceStatusIcon.tsx | Extracts/centralizes workspace sidebar status badge rendering. |
| src/ui/src/components/sidebar/Sidebar.tsx | Uses WorkspaceStatusIcon; changes share button behavior to open modal only; updates synthetic messages with author fields. |
| src/ui/src/components/shared/WorkspacePanelHeader.tsx | Adds ParticipantsRoster to header; derives selfParticipantId. |
| src/ui/src/components/settings/SettingsSidebar.tsx | Adds “Collaboration” settings section entry. |
| src/ui/src/components/settings/SettingsPage.tsx | Lazy-loads collaboration settings section. |
| src/ui/src/components/settings/sections/CollaborationSettings.tsx | New settings page for display name + default consensus preference. |
| src/ui/src/components/modals/ShareModal.tsx | Replaces legacy modal with workspace-scoped share list + “mint share” form. |
| src/ui/src/components/modals/ConfirmSetupScriptModal.tsx | Updates emitted system messages to include author fields. |
| src/ui/src/components/command-palette/CommandPalette.tsx | Updates emitted system messages to include author fields. |
| src/ui/src/components/chat/SessionTabs.tsx | Lists chat sessions via remote RPC for remote workspaces. |
| src/ui/src/components/chat/planFilePath.test.ts | Updates test fixtures for author fields. |
| src/ui/src/components/chat/PlanApprovalCard.tsx | Adds consensus progress display; supports remote plan reads via read_plan_file. |
| src/ui/src/components/chat/ParticipantsRoster.tsx | New participant roster with host-only moderation controls (kick/mute). |
| src/ui/src/components/chat/MessagesWithTurns.tsx | Renders author-stamped user messages (“You” vs display name). |
| src/ui/src/components/chat/ChatPanel.tsx | Calls join_session for remote collaborative sessions; updates optimistic messages with author fields. |
| src/ui/src/components/chat/ChatInputArea.tsx | Disables composer based on collab turn-lock state; adds locked placeholders/titles. |
| src/ui/src/App.tsx | Hydrates collaboration preferences from app settings on boot. |
| src/room.rs | New room + registry implementation with broadcast channel, turn lock, and on-create hook + tests. |
| src/model/chat_message.rs | Adds author fields to Rust ChatMessage model. |
| src/migrations/mod.rs | Registers new migration for chat message author columns. |
| src/migrations/20260428034257_chat_message_author.sql | Adds nullable author columns to chat_messages. |
| src/lib.rs | Exposes new room module. |
| src/fork.rs | Ensures forked chat history copies author fields. |
| src/db/test_support.rs | Updates DB test helper to populate new author fields. |
| src/db/chat.rs | Persists/reads new author fields; updates column list constant. |
| src/chat.rs | Ensures assistant/sentinel messages have null author fields. |
| src-tauri/src/transport/ws.rs | Parses participant_id from server auth responses. |
| src-tauri/src/state.rs | Adds shared RoomRegistry, share server config state, and host display-name resolver. |
| src-tauri/src/remote.rs | Adds derived participant_id field and helper for remote connections. |
| src-tauri/src/main.rs | Installs room on-create hook; hydrates persisted shares; wires new share commands. |
| src-tauri/src/commands/share.rs | Implements workspace-scoped share lifecycle + persisted hydration. |
| src-tauri/src/commands/remote.rs | Adds participant-id plumbing, improves remote error propagation, adds kick/mute commands; updates subprocess readiness detection. |
| src-tauri/src/commands/mod.rs | Registers new share commands module. |
| src-tauri/src/commands/chat/send.rs | Stamps/broadcasts user messages in collab; enforces turn lock; broadcasts agent stream + permission prompts + vote opened/resolved. |
| src-tauri/src/commands/chat/mod.rs | Re-exports record_plan_vote for host-side resolver task. |
| src-tauri/src/commands/chat/lifecycle.rs | Updates system messages to include author fields. |
| src-tauri/src/commands/chat/interaction.rs | Implements consensus vote recording + resolution; broadcasts vote events. |
| src-server/tests/env_provider_integration.rs | Updates server state construction to include config. |
| src-server/src/ws.rs | Adds shared rooms + live config to state; constructs per-connection ctx; drops participants on disconnect. |
| src-server/src/main.rs | Removes legacy CLI subcommands; runs server with new options shape. |
| src-server/src/lib.rs | Adds run_with_rooms and existing_config support; removes global connection-string printing. |
| src-server/src/handler.rs | Adds per-connection authorization context; enforces share scoping + revocation; integrates collab RPC dispatch; broadcasts through rooms when present. |
| src-server/src/collab.rs | New server-side collab RPC handlers (join/leave/vote + disconnect cleanup). |
| src-server/src/auth.rs | Implements share-scoped auth model and participant-id derivation; adds tests. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Addresses Copilot's review comments on PR #612 — three correctness bugs, three doc-comment drifts, three i18n gaps, and a small hook extraction that prevents the worst of the bugs from recurring. Bug fixes: - ChatInputArea.tsx: `lockedByOther` compared against `"host"` literal, which disabled the composer + Stop button for the actual turn holder on remote clients (their pid is the remote-issued string, not `"host"`). Now compares against the workspace's self-pid via `useSelfParticipantId`. - PlanApprovalCard.tsx: same root cause in `ConsensusProgress` — the "(you)" label hardcoded `voter.id === "host"`, mislabelling the host as "you" on every remote viewer. Now compares against self-pid. - handler.rs: server-side stream bridge declared `latest_usage` and consumed it via `take()` but never *populated* it — every assistant message persisted from a remote-initiated turn had null token counts. Mirror the Tauri-side capture pattern from `StreamEvent::Stream { InnerStreamEvent::MessageDelta }`. Hook extraction: - New `useSelfParticipantId(workspaceId)` in `src/ui/src/hooks/` centralizes the "for this workspace, who am I?" derivation that was inline-duplicated across four call sites. Future collab UI consumers can't accidentally hardcode `"host"` again. - WorkspacePanelHeader, MessagesWithTurns, ChatInputArea, and PlanApprovalCard all now consume this hook. Doc comment drifts (no behavior change): - `types/chat.ts`: clarify `author_participant_id` is `"host"` (not NULL) for host-authored messages in collab sessions; NULL is reserved for solo / 1:1 / pre-collab rows. - The migration's leading comment: same correction. - `commands/share.rs`: replace the obsolete TODO comment that predated the `set_on_create` hook this PR added. i18n: - Add `composer_locked_placeholder`, `composer_locked_title`, `user_label` keys to `chat.json`. - Replace hardcoded "Another user is asking…" / "Waiting for X to finish their turn" / "User" fallback strings with `t(...)` calls using the new keys.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 58 out of 58 changed files in this pull request and generated 8 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Addresses 8 new Copilot comments on commit 0b077a8 — one security fix, one correctness fix, four polish/consistency fixes, and two i18n coverage gaps. Security (#3189160026): - read_plan_file now requires a chat_session_id and gates through ctx_authorize_chat_session, then checks the canonicalized path starts_with the workspace's worktree. A scoped share can no longer read plan files from other worktrees on the host. Correctness (#3189160061): - New prune_consensus_voters_for_session re-evaluates open plan votes when the participant set changes. Implicit-abstain semantics: a departed required-voter is dropped from required_voters AND has their cast vote removed; if remaining required_voters are now all approve, the vote finalizes. Wired via the room's participants-changed event in spawn_host_vote_resolver. Without this, a single disconnect could deadlock the agent indefinitely. Polish: - chat-message-added listener (#3189159789) now uses the shared selfParticipantIdForWorkspace helper instead of inlining the derivation. Hook + helper share one body so render-context and event-listener-context callers stay in lockstep. - model/chat_message.rs (#3189159937): updated doc comment to match the same semantics already corrected in TS / SQL. - turn-started broadcast (#3189159970) now uses resolve_host_display_name() instead of hardcoded "Host" so the user-visible composer-locked banner matches the chat-message author chip and the roster entry. - ShareButton (#3189160105): drives off the new activeSharesCount store slice (hydrated at startup, kept in sync by ShareModal's refresh) instead of the legacy localServerRunning flag. i18n: - ShareModal (#3189159880): all user-facing strings moved to modals.json with proper t(...) calls. - CollaborationSettings (#3189159922): all labels/descriptions/ placeholders moved to settings.json.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 69 out of 69 changed files in this pull request and generated 6 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
LETS GO ALREADY!!! |
Addresses Copilot's review comments on PR #612 — three correctness bugs, three doc-comment drifts, three i18n gaps, and a small hook extraction that prevents the worst of the bugs from recurring. Bug fixes: - ChatInputArea.tsx: `lockedByOther` compared against `"host"` literal, which disabled the composer + Stop button for the actual turn holder on remote clients (their pid is the remote-issued string, not `"host"`). Now compares against the workspace's self-pid via `useSelfParticipantId`. - PlanApprovalCard.tsx: same root cause in `ConsensusProgress` — the "(you)" label hardcoded `voter.id === "host"`, mislabelling the host as "you" on every remote viewer. Now compares against self-pid. - handler.rs: server-side stream bridge declared `latest_usage` and consumed it via `take()` but never *populated* it — every assistant message persisted from a remote-initiated turn had null token counts. Mirror the Tauri-side capture pattern from `StreamEvent::Stream { InnerStreamEvent::MessageDelta }`. Hook extraction: - New `useSelfParticipantId(workspaceId)` in `src/ui/src/hooks/` centralizes the "for this workspace, who am I?" derivation that was inline-duplicated across four call sites. Future collab UI consumers can't accidentally hardcode `"host"` again. - WorkspacePanelHeader, MessagesWithTurns, ChatInputArea, and PlanApprovalCard all now consume this hook. Doc comment drifts (no behavior change): - `types/chat.ts`: clarify `author_participant_id` is `"host"` (not NULL) for host-authored messages in collab sessions; NULL is reserved for solo / 1:1 / pre-collab rows. - The migration's leading comment: same correction. - `commands/share.rs`: replace the obsolete TODO comment that predated the `set_on_create` hook this PR added. i18n: - Add `composer_locked_placeholder`, `composer_locked_title`, `user_label` keys to `chat.json`. - Replace hardcoded "Another user is asking…" / "Waiting for X to finish their turn" / "User" fallback strings with `t(...)` calls using the new keys.
Addresses 8 new Copilot comments on commit 0b077a8 — one security fix, one correctness fix, four polish/consistency fixes, and two i18n coverage gaps. Security (#3189160026): - read_plan_file now requires a chat_session_id and gates through ctx_authorize_chat_session, then checks the canonicalized path starts_with the workspace's worktree. A scoped share can no longer read plan files from other worktrees on the host. Correctness (#3189160061): - New prune_consensus_voters_for_session re-evaluates open plan votes when the participant set changes. Implicit-abstain semantics: a departed required-voter is dropped from required_voters AND has their cast vote removed; if remaining required_voters are now all approve, the vote finalizes. Wired via the room's participants-changed event in spawn_host_vote_resolver. Without this, a single disconnect could deadlock the agent indefinitely. Polish: - chat-message-added listener (#3189159789) now uses the shared selfParticipantIdForWorkspace helper instead of inlining the derivation. Hook + helper share one body so render-context and event-listener-context callers stay in lockstep. - model/chat_message.rs (#3189159937): updated doc comment to match the same semantics already corrected in TS / SQL. - turn-started broadcast (#3189159970) now uses resolve_host_display_name() instead of hardcoded "Host" so the user-visible composer-locked banner matches the chat-message author chip and the roster entry. - ShareButton (#3189160105): drives off the new activeSharesCount store slice (hydrated at startup, kept in sync by ShareModal's refresh) instead of the legacy localServerRunning flag. i18n: - ShareModal (#3189159880): all user-facing strings moved to modals.json with proper t(...) calls. - CollaborationSettings (#3189159922): all labels/descriptions/ placeholders moved to settings.json.
65e50dd to
7ef8a1e
Compare
Addresses Copilot's review comments on PR #612 — three correctness bugs, three doc-comment drifts, three i18n gaps, and a small hook extraction that prevents the worst of the bugs from recurring. Bug fixes: - ChatInputArea.tsx: `lockedByOther` compared against `"host"` literal, which disabled the composer + Stop button for the actual turn holder on remote clients (their pid is the remote-issued string, not `"host"`). Now compares against the workspace's self-pid via `useSelfParticipantId`. - PlanApprovalCard.tsx: same root cause in `ConsensusProgress` — the "(you)" label hardcoded `voter.id === "host"`, mislabelling the host as "you" on every remote viewer. Now compares against self-pid. - handler.rs: server-side stream bridge declared `latest_usage` and consumed it via `take()` but never *populated* it — every assistant message persisted from a remote-initiated turn had null token counts. Mirror the Tauri-side capture pattern from `StreamEvent::Stream { InnerStreamEvent::MessageDelta }`. Hook extraction: - New `useSelfParticipantId(workspaceId)` in `src/ui/src/hooks/` centralizes the "for this workspace, who am I?" derivation that was inline-duplicated across four call sites. Future collab UI consumers can't accidentally hardcode `"host"` again. - WorkspacePanelHeader, MessagesWithTurns, ChatInputArea, and PlanApprovalCard all now consume this hook. Doc comment drifts (no behavior change): - `types/chat.ts`: clarify `author_participant_id` is `"host"` (not NULL) for host-authored messages in collab sessions; NULL is reserved for solo / 1:1 / pre-collab rows. - The migration's leading comment: same correction. - `commands/share.rs`: replace the obsolete TODO comment that predated the `set_on_create` hook this PR added. i18n: - Add `composer_locked_placeholder`, `composer_locked_title`, `user_label` keys to `chat.json`. - Replace hardcoded "Another user is asking…" / "Waiting for X to finish their turn" / "User" fallback strings with `t(...)` calls using the new keys.
Addresses 8 new Copilot comments on commit 0b077a8 — one security fix, one correctness fix, four polish/consistency fixes, and two i18n coverage gaps. Security (#3189160026): - read_plan_file now requires a chat_session_id and gates through ctx_authorize_chat_session, then checks the canonicalized path starts_with the workspace's worktree. A scoped share can no longer read plan files from other worktrees on the host. Correctness (#3189160061): - New prune_consensus_voters_for_session re-evaluates open plan votes when the participant set changes. Implicit-abstain semantics: a departed required-voter is dropped from required_voters AND has their cast vote removed; if remaining required_voters are now all approve, the vote finalizes. Wired via the room's participants-changed event in spawn_host_vote_resolver. Without this, a single disconnect could deadlock the agent indefinitely. Polish: - chat-message-added listener (#3189159789) now uses the shared selfParticipantIdForWorkspace helper instead of inlining the derivation. Hook + helper share one body so render-context and event-listener-context callers stay in lockstep. - model/chat_message.rs (#3189159937): updated doc comment to match the same semantics already corrected in TS / SQL. - turn-started broadcast (#3189159970) now uses resolve_host_display_name() instead of hardcoded "Host" so the user-visible composer-locked banner matches the chat-message author chip and the roster entry. - ShareButton (#3189160105): drives off the new activeSharesCount store slice (hydrated at startup, kept in sync by ShareModal's refresh) instead of the legacy localServerRunning flag. i18n: - ShareModal (#3189159880): all user-facing strings moved to modals.json with proper t(...) calls. - CollaborationSettings (#3189159922): all labels/descriptions/ placeholders moved to settings.json.
7ef8a1e to
4ad316f
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 73 out of 73 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Addresses Copilot's review comments on PR #612 — three correctness bugs, three doc-comment drifts, three i18n gaps, and a small hook extraction that prevents the worst of the bugs from recurring. Bug fixes: - ChatInputArea.tsx: `lockedByOther` compared against `"host"` literal, which disabled the composer + Stop button for the actual turn holder on remote clients (their pid is the remote-issued string, not `"host"`). Now compares against the workspace's self-pid via `useSelfParticipantId`. - PlanApprovalCard.tsx: same root cause in `ConsensusProgress` — the "(you)" label hardcoded `voter.id === "host"`, mislabelling the host as "you" on every remote viewer. Now compares against self-pid. - handler.rs: server-side stream bridge declared `latest_usage` and consumed it via `take()` but never *populated* it — every assistant message persisted from a remote-initiated turn had null token counts. Mirror the Tauri-side capture pattern from `StreamEvent::Stream { InnerStreamEvent::MessageDelta }`. Hook extraction: - New `useSelfParticipantId(workspaceId)` in `src/ui/src/hooks/` centralizes the "for this workspace, who am I?" derivation that was inline-duplicated across four call sites. Future collab UI consumers can't accidentally hardcode `"host"` again. - WorkspacePanelHeader, MessagesWithTurns, ChatInputArea, and PlanApprovalCard all now consume this hook. Doc comment drifts (no behavior change): - `types/chat.ts`: clarify `author_participant_id` is `"host"` (not NULL) for host-authored messages in collab sessions; NULL is reserved for solo / 1:1 / pre-collab rows. - The migration's leading comment: same correction. - `commands/share.rs`: replace the obsolete TODO comment that predated the `set_on_create` hook this PR added. i18n: - Add `composer_locked_placeholder`, `composer_locked_title`, `user_label` keys to `chat.json`. - Replace hardcoded "Another user is asking…" / "Waiting for X to finish their turn" / "User" fallback strings with `t(...)` calls using the new keys.
Addresses 8 new Copilot comments on commit 0b077a8 — one security fix, one correctness fix, four polish/consistency fixes, and two i18n coverage gaps. Security (#3189160026): - read_plan_file now requires a chat_session_id and gates through ctx_authorize_chat_session, then checks the canonicalized path starts_with the workspace's worktree. A scoped share can no longer read plan files from other worktrees on the host. Correctness (#3189160061): - New prune_consensus_voters_for_session re-evaluates open plan votes when the participant set changes. Implicit-abstain semantics: a departed required-voter is dropped from required_voters AND has their cast vote removed; if remaining required_voters are now all approve, the vote finalizes. Wired via the room's participants-changed event in spawn_host_vote_resolver. Without this, a single disconnect could deadlock the agent indefinitely. Polish: - chat-message-added listener (#3189159789) now uses the shared selfParticipantIdForWorkspace helper instead of inlining the derivation. Hook + helper share one body so render-context and event-listener-context callers stay in lockstep. - model/chat_message.rs (#3189159937): updated doc comment to match the same semantics already corrected in TS / SQL. - turn-started broadcast (#3189159970) now uses resolve_host_display_name() instead of hardcoded "Host" so the user-visible composer-locked banner matches the chat-message author chip and the roster entry. - ShareButton (#3189160105): drives off the new activeSharesCount store slice (hydrated at startup, kept in sync by ShareModal's refresh) instead of the legacy localServerRunning flag. i18n: - ShareModal (#3189159880): all user-facing strings moved to modals.json with proper t(...) calls. - CollaborationSettings (#3189159922): all labels/descriptions/ placeholders moved to settings.json.
e4e39aa to
ca2e0ee
Compare
Add workspace-scoped sharing for remote Claudette sessions, including pairing-token grants, live share configuration, immediate revocation, and per-workspace RPC authorization. Implement collaborative chat rooms with participant identity, host and remote roster state, turn locking, user-message broadcasting, agent-stream fanout, resync handling, and session snapshot hydration for reconnects. Add consensus support for ExitPlanMode and AskUserQuestion, including required-voter snapshots, vote and answer propagation, late-joiner observer handling, muted and left participant pruning, plan approval restoration, and vote badges in the UI. Synchronize remote client state for active turns, prompt timers, selected model, plan mode, forks, rollbacks, archived workspaces, and pending plan or question prompts. Add the workspace sharing modal, collaboration settings, participant roster and moderation controls, self-participant labeling, remote-safe author labels, and localized collaboration, chat, and share strings. Harden developer tooling by making the macOS hot-reload app runner and dev launcher restart cleanly without duplicate or stale-process kills. Add coverage for remote session snapshots, collaborative answer and plan submission routing, author labeling, consensus vote rules, plan path snapshot lookup, checkpoint workspace detection, workspace archive propagation, and store collaboration state.
7ed4da1 to
25c94ce
Compare
| let prefix = &content[..marker_idx]; | ||
| let start = prefix.rfind('/').unwrap_or(0); |
| @@ -1303,6 +1425,12 @@ export function ChatPanel() { | |||
| // toolbar chip in sync). | |||
| setPlanMode(sid, false); | |||
Re-pairing the same host (via the Nearby list or by pasting another connection string) used to insert a fresh remote_connections row with a new uuid every time, leaving the prior entry behind in the sidebar holding a session token the server has rotated past. The user-visible result was a stale "unusable" connection stacked under a fresh duplicate. `pair_with_server` now looks up an existing row by (host, port) and refreshes its name, session token, and cert fingerprint in place, preserving id and created_at so the sidebar entry keeps its identity. The frontend store action mirrors the backend by upserting by id, so even paths that bypass list_remote_connections (mid-session re-pairs) don't shadow the refreshed row with a duplicate.
Summary
Extends Claudette's 1:1 remote-control sharing into multi-user collaborative workspace sharing. Multiple participants can now join the same chat session, see each other's messages live, and (optionally) gate plan approval on unanimous consensus. The host issues workspace-scoped shares (rather than instance-wide pairing tokens), so each share grants access only to a specific subset of workspaces; revoking a share invalidates every session token it issued.
flowchart LR subgraph host["Host process (Tauri)"] HU[Host UI<br/>Zustand store] HB[Bridge<br/>chat/send.rs] SS[Embedded server<br/>claudette-server] RR[(RoomRegistry<br/>shared Arc)] end subgraph remote["Remote (paired)"] RU[Remote UI<br/>Zustand store] end HB -->|publish| RR SS -->|publish<br/>subscribe| RR RR -->|on_create hook| HU RR -->|tokio::broadcast| RU RU -.->|join_session<br/>send_chat_message<br/>vote_plan_approval| SSEvery collaborative session has a
Roomthat owns a boundedtokio::broadcastchannel. Both the Tauri host and the embedded server publish into the same room and subscribe from it; events fan out to every participant — host UI plus all remote clients — through a single source of truth. Solo / 1:1 legacy sessions skip the room entirely and keep the existing direct-emit path (zero overhead for non-collaborative use).Headline new capabilities
handler.rs::handle_request).turn-started).Architecture: see
src/room.rsfor theRoom/RoomRegistrycore,src-server/src/collab.rsfor the server-side join/leave/vote handlers, andsrc-tauri/src/commands/share.rsfor share lifecycle. Theagent-streamevent payload is now a singleclaudette::chat::AgentStreamPayloadstruct serialized by both bridges (Tauri-side and server-side), so the wire shape can no longer drift between them.15 commits, ~+4250 / −520 across ~58 files (3 new Rust modules, 4 new TS components, 1 SQL migration adding nullable
author_participant_id/author_display_nametochat_messages).Complexity Notes
6a0c104viaRoomRegistry::set_on_createhook).tokio::sync::broadcastdoes not buffer for late subscribers — if a publisher fires before a subscriber attaches, that subscriber will never see the event. The on_create hook fires synchronously on room creation before the newArc<Room>becomes visible in the registry, so the host's mirror task captures aReceiverbefore any handler can publish. Locked in by a regression test insrc/room.rs.src-tauri/src/commands/chat/send.rsandsrc-server/src/handler.rs::handle_send_chat_messagepublishagent-streamevents into the room. Afterdd5c105they share a singleclaudette::chat::AgentStreamPayloadstruct withserde::Serializeso the wire shape stays in lockstep. Earlier in this branch the server side hand-rolledjson!and one regression dropped events on the remote until1e1db36fixed the key — that whole class of bug is now structurally prevented.Arc<RoomRegistry>sharing: the Tauri host and embedded server live in the same OS process but use distinctStatetypes. They share the registry viarun_with_rooms(opts, rooms)insrc-server/src/lib.rs. Worth verifying during review that no future refactor accidentally constructs a second registry on the server side — the on_create hook would fail to fire and we'd silently regress to the publish-before-subscribe race.ALTER TABLE chat_messages ADD COLUMN ... NULL) — existing rows read back withNULLauthor fields, treated as"host"legacy.submit_agent_answercollaboration RPC, using the same consensus snapshot and vote propagation path as other shared prompts. This lets every required voter answer before the agent continues.Test Steps
Setup: Restart
cargo tauri devon the host. To exercise multi-participant flows, also runcargo tauri devon a second machine (or VM) with this branch checked out.state.rooms.get(...)returnsNoneand code paths fall back to the original direct-emit logic).Sharebutton). ClickNew share, pick at least one workspace, checkCollaborative modeandRequire unanimous plan approval, clickMint share. A connection stringclaudette://host:port/<token>should appear; copy it.Add remote. Paste the connection string. The shared workspace appears underSEANS-MACBOOK-PRO.LOCAL(or your hostname). Open it, then open a chat session.1 connected. The chat panel header should show both participants in the roster.ping. The user message should render on both ends; the agent's response should stream into both UIs in lockstep. Repeat from the host.User X is asking…indicator. After the turn ends, both composers re-enable.Plan how you'd refactor X — use ExitPlanMode). When the plan card appears on both ends, clickView planfrom the remote — the plan content should expand inline. Then have the remote clickDenywith a critique. The agent should receive the critique and produce a revised plan on the next turn. Test the inverse: both Approve → agent proceeds.Denyfirst — the vote should short-circuit immediately even if the remote hasn't voted.cargo tauri dev, restart it, and reopen the share modal. The previously-minted share should appear immediately (terminal logs[share] Hydrating N persisted share(s)).Stopon the share. The remote's next RPC should fail withThis share has been revoked by the host.Checklist
src/room.rs; collab slice + reducer tests insrc/ui/src/stores/useAppStore.test.ts; existing path-validation inread_plan_fileserver handler) — full integration test for multi-participant flow still pending[collab-trace]eprintlns removed (commiteafb92a)AgentStreamPayloadextracted to sharedclaudette::chatmodule (commitdd5c105)